/**
*    Copyright (C) 2008  Chase Kernan 
*    chase.kernan@gmail.com
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*             
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
* 
*/

package com.chasekernan.hxnova.server.functions.battles;

import com.chasekernan.hxnova.core.battles.BattlePlan;
import com.chasekernan.hxnova.core.designs.HullDesign;
import com.chasekernan.hxnova.core.fleets.Fleet;
import com.chasekernan.hxnova.core.dataholders.Global;
import com.chasekernan.hxnova.core.minerals.MineralSet;
import com.chasekernan.hxnova.core.players.Player;
import com.chasekernan.hxnova.core.players.PlayerTurnFile;
import com.chasekernan.hxnova.server.Server;
import com.chasekernan.hxnova.core.stars.Star;
import com.chasekernan.hxnova.utils.IntVector;
import com.chasekernan.hxnova.core.battles.BattleRecord;
import com.chasekernan.hxnova.core.components.Component;
import com.chasekernan.hxnova.core.components.BeamWeapon;
import com.chasekernan.hxnova.core.components.Missile;
import com.chasekernan.hxnova.core.stars.Concentrations;
import com.chasekernan.hxnova.core.components.Computer;
import com.chasekernan.hxnova.core.designs.HullDesign;
import haxe.Stack;

enum BattleParticipant {
    FleetParticipant(f : Fleet);
    StarbaseParticipant(s : Star);
}

typedef BattlesList = List < List < BattleParticipant >> ;

typedef Defences = {
    var armor : Float;
    var shields : Float;
};

//used to sort firing by initiative
typedef WeaponSlot = {
    var slot : DesignPart;
    var parent : BattleStack;
    var initiative : Int;
};

//TODO: ALLOW TECH GAIN FROM BATTLTES

/**
    Handles fleet battles and records them to a BattleRecord.
**/
class Battles {
    
    /**
        If a stack is already targeted, then its attractiveness is multiplied
        by this for other stacks.
    **/
    public static var TARGETED_MULTIPLIER = 0.5;
    
    /**
        The starting positions of players in a battle.
        Usage: if there are n players in the battle then the positions are
        given by [STARTING_POSITIONS[n - 2]].
    **/
    public static var STARTING_POSITIONS = [
            [iv(1, 4), iv(8, 5)],
            [iv(4, 1), iv(8, 8), iv(1, 8)],
            [iv(1, 1), iv(8, 8), iv(1, 8), iv(8, 1)],
            [iv(4, 1), iv(6, 8), iv(1, 4), iv(8, 4), iv(2, 8)],
            [iv(1, 4), iv(8, 5), iv(2, 8), iv(7, 1), iv(6, 8), iv(3, 1)],
            [iv(1, 1), iv(1, 5), iv(2, 8), iv(6, 8), iv(8, 6), iv(8, 2), iv(5, 1)],
            [iv(1, 3), iv(1, 6), iv(3, 8), iv(6, 8), iv(8, 6), iv(8, 3), iv(6, 1), iv(3, 1)]
        ];
        
    //just a shortening here
    private static inline function iv(x : Int, y : Int) : IntVector {
        return new IntVector(x, y);
    }
    
    /**
        The distance (squared) between 2 fleets needs to be this small in order
        to have a battle occur.
    **/
    public static var DISTANCE_FOR_BATTLE = 0.5;
    
    /**
        There can only be this many rounds in a battle.
    **/
    public static var MAX_BATTLE_ROUNDS = 16;
    
    /**
        The mass of a stack can vary by this percentage in determining
        the movement order.
    **/
    public static var MASS_VARIANCE = 0.1;
    
    /**
        The maximum amount a stack can move in a round.
    **/
    public static var MAX_ROUND_MOVEMENT = 3;
    
    /**
        The number of times a stack has to move in order to disengage.
    **/
    public static var MOVES_NEEDED_FOR_DISENGAGE = 4;
    
    /**
        The rate at which beam weapons decay as they move farther away from 
        their targets.
    **/
    public static var BEAM_DECAY_RATE = 0.1;
    
    /**
        The percent damage a missile does to shields even though it missed
    **/
    public static var MISSED_MISSILE_DAMAGE = 1 / 8;
    
    /**
        The multiplier for capital missiles when the target stack has no shields
        left.
    **/
    public static var CAPITAL_MISSILE_MULTIPLIER = 2;
    
    //these are only public so that battle stack can access them
    public static var playersInBattle : List<Player>;
    public static var record : BattleRecord;
    public static var disengagedStacks : Array<BattleStack>;
    public static var stacks : Array<BattleStack>;

    public static function run() {
        var battles = determineBattles().filter(filterPossibleBattles);
        
        for (battle in battles) {
            updateStacks(battle);
            disengagedStacks = new Array();
            
            saveHullDesigns(); //all designs are visible to every player
            record = new BattleRecord(stacks, playersInBattle);
            
            for (round in 0...MAX_BATTLE_ROUNDS) {
                record.recordEvent(StartRound(round));
                setupEnemies();
                doMovement(round);
                doFiring();
            }
            
            record.finalizeBattle(stacks);
            
            //add any stacks that aren't dead back to the fleets/stars.
            //any fleets that are empty in the end are removed
            reconstructFleetsAndStars();
        }
        
        resetShields();
    }
    
    private static function reconstructFleetsAndStars() {
        stacks = stacks.concat(disengagedStacks);
        for (stack in stacks) {
            if (stack.isDestroyed || stack.amount < 1) continue;
            
            switch (stack.parent) {
                
                case FleetParticipant(f):
                    //used Math.ciel for armor so that it doesn't get rounded 
                    //to 0.
                    f.ships.push({ amount : stack.amount, type : stack.type, 
                                   armor : Math.ceil(stack.armor), 
                                   shields : Math.round(stack.shields), 
                                   battlePlan : stack.battlePlan });
                    f.fuel += stack.fuel;
                    f.colonists += stack.colonists;
                    f.minerals.add(stack.minerals);
                    
                case StarbaseParticipant(s):
                    s.starbase = StarStarbase.Design(stack.type);
            }
        }
        
        //remove fleets that are empty
        for (fleet in Global.fleets.getAllFleets()) {
            if (fleet.ships.length == 0) Global.fleets.removeParticle(fleet);
        }
    }
    
    //sets shields back to their maximum
    private static function resetShields() {
        for (fleet in Global.fleets) {
            for (stack in fleet.ships) {
                stack.shields = stack.amount * stack.type.getShields();
            }
        }
    }
    
    private static function setupEnemies() {
        for (stack in stacks) stack.updateEnemyStacks();
    }
    
    private static function doMovement(round : Int) {
        //sort by mass with slight variance
        for (stack in stacks) stack.randomizeWeight();
        
        //NOTE: This is sorted from lightest to heaviest, the opposite of what
        //actually happens, as this iterates over the array backwards in order 
        //to be able to remove disengaged stacks.
        stacks.sort(
            function(stack1 : BattleStack, stack2 : BattleStack) : Int { 
                return if (stack1.battleMass < stack2.battleMass) 1 else -1;
            }
        );
        
        for (i in 0...MAX_ROUND_MOVEMENT) {
            //if a stack can move at least this many squares, then it can move
            //this iteration
            var maxMove = MAX_ROUND_MOVEMENT - i;
            
            var i = stacks.length;
            while(i-- > 0) {
                var stack = stacks[i];
                
                if (stack.getRoundMovement(round) >= maxMove) {
                    //move the stack.
                    //if moveStack returned true, the stack has disengaged
                    var disengaged = stack.doMovement();
                    if (disengaged) {
                        record.recordEvent(StackDisengage(stack));
                        stacks.splice(i, 1);
                        disengagedStacks.push(stack);
                    } else {
                        record.recordEvent(StackMove(stack, stack.location));
                    }     
                }
            }
        }
    }
    
    private static function doFiring() {
        var slots = new Array<WeaponSlot>();
        
        for (stack in stacks) slots = slots.concat(stack.getWeaponSlots());
        
        //sorts from highest initiative to lowest
        slots.sort(function (slot1 : WeaponSlot, slot2 : WeaponSlot) : Int {
            return slot1.initiative - slot2.initiative; //
        });
        
        for (slot in slots) {
            if (slot.parent.isDestroyed) continue;
            
            var target = slot.parent.getTarget(slot.slot);
            if (target == null || target.target == null) continue;
            
            record.recordEvent(WeaponFire(slot.parent, 
                                          slot.slot, 
                                          target.target));
            
            var damage = slot.parent.getDamageDoneBySlot(slot.slot, 
                                                         target.target);
            applyDamage(target.target, slot.parent, damage.armor, 
                        damage.shields);
        }
    }
    
    private static function applyDamage(target : BattleStack, 
                                        attacker : BattleStack, 
                                        armorDamage : Float, 
                                        shieldDamage : Float, 
                                        ?overflowDamage = 0.0) {
        if (overflowDamage > 0.0) {
            var overToShields = Math.min(target.shields - shieldDamage, 
                                         overflowDamage);
            shieldDamage += overToShields;
            overflowDamage -= overToShields;
        }
        
        armorDamage += overflowDamage;
        
        record.recordEvent(
                BattleEvent.DamageDone(target, shieldDamage, 
                                       Math.min(target.armor, 
                                                armorDamage)));
                                                         
        target.shields -= shieldDamage;
            
        //armor is tricky, if theres enough damage to destroy a ship in the
        //stack, it destroys that ship, otherwise it damages the whole 
        //stack.
        //
        //it gets even worse with missiles/beamers. Missiles can only 
        //destroy one ship at a time, and the reported damage is already
        //limited by that value. Beamers on the other hand, can stream 
        //damage over to stacks on the same square.
            
        if (armorDamage < target.armor - 0.0001) {
            var indivArmor = target.getArmorOnIndividualShip();
            var shipsDestroyed = Math.floor(armorDamage / indivArmor);
                
            record.recordEvent(ShipsDestroyed(target, 
                                              shipsDestroyed));
                
            target.amount -= shipsDestroyed;
            target.armor -= armorDamage;
            target.maxArmor -= shipsDestroyed * 
                               target.type.getArmor();
            target.maxShields -= shipsDestroyed * 
                                 target.type.getShields();
        } else {
            target.isDestroyed = true;
                
            record.recordEvent(StackDestroyed(target));
            stacks.remove(target);
                
            //overflow
            if (armorDamage > target.armor + 0.0001) {
                for (stack in stacks) {
                    if (stack.isDestroyed) continue;
                        
                    if (stack.location.equals(target.location) && 
                            attacker.willTarget(stack)) {
                        Battles.applyDamage(stack, attacker, 0.0, 0.0, 
                                            armorDamage);
                    }
                }
            }
        }
    }
    
    /**
        Takes the base accuracy (in the range 0.0 - 100.0), the computers used
        (with benefits in the range 0.0 - 100.0), (TODO: an array of jammers),
        and returns the final accuracy in the range 0.0 - 1.0.
        
        TODO: Make the component accuracy fields in the range 0.0 - 1.0.
        
        http://wiki.gible.net/index.php?title=BATTLE.TXT
    **/
    public static function getAccuracy(base : Float, 
                                       computers : Array<Computer>) : Float {
        if (computers.length == 0) return base;
        
        var computing = 0.0;
        for (computer in computers) {
            if (computing == 0.0) computing += computer.accuracy / 100.0;
            else computing *= 1.0 + computer.accuracy / 100.0;
        }
        
        var jamming = 0.0; //TODO: jamming
        
        var totalEffect = computing - jamming;
        
        //if the effect is positive, decrease the inaccuracy
        //100% - (100% - base%) * (100% - effect%)
        //ex: base = 75%, totalEffect = 50% -> 87.5% accuracy
        
        //if the effect is negative, the accuracy is mutliplied by 1 - 
        //[totalEffect] (its 1.0 + totalEffect, because totalEffect is negative
        //when jamming is stronger).
        //ex: base 75%, totalEffect = -15% -> 63.75% accuracy
        
        return if (totalEffect > 0.0) 
                    1.0 - ((1.0 - base / 100.0) * (1.0 - totalEffect))
               else 
                    (base / 100.0) * (1.0 + totalEffect);    
    }
    
    private static function saveHullDesigns() {
        for (player in playersInBattle) {
            var turnFile = Server.playerTurns[player.id];
            
            for (stack in stacks) {
                var stackDesign = stack.type;
                var owner = switch (stack.parent) {
                    case FleetParticipant(fleet): fleet.owner;
                    case StarbaseParticipant(star): star.owner;
                };
                
                //no need to save the player's own stacks
                if (owner.id == player.id) continue;
                
                //add the design, if its not already there.
                try {
                    //this should throw an error if the design isn't 
                    //already saved this turn
                    var design = turnFile.players.getPlayerById(owner.id)
                                 .getDesignByName(stackDesign.name);
                                 
                    //the null check here is just an added precaution
                    if (design == null) throw "Error";
                    
                } catch (e : Dynamic) {
                    turnFile.players.getPlayerById(owner.id)
                                    .addDesign(stackDesign.clone());
                }
            }
        }
    }
    
    //Uses [playersInvolved] to return the starting positions of each player as
    //an [IntHash<IntVector>]
    private static function getStartingPositions() {
        var positions = STARTING_POSITIONS[playersInBattle.length - 2];
        if (positions == null) {
            throw "No starting positions set for " + playersInBattle.length +
                  " players.";
        }
        
        var positionsHash = new IntHash<IntVector>();
        var positionId = 0;
        for (player in playersInBattle) {
            positionsHash.set(player.id, positions[positionId]);
            positionId++;
        }
        
        return positionsHash;
    }
    
    //Takes the list of participants in a battle and sets stacks.
    //Also removes the ships/starbases in the stacks from the game and the ones
    //that survive will be added back.
    private static function updateStacks(battle : List<BattleParticipant>) {
        stacks = new Array<BattleStack>();
        BattleStack.clearIdCount();
        var startingPositions = getStartingPositions();
        
        for (participant in battle) {
            switch (participant) {
                    
                case FleetParticipant(f):
                    var position = startingPositions.get(f.owner.id);
                    //need a [while] loop as it removes the ship type from the
                    //fleet as it goes through
                    var i = f.ships.length;
                    while (i-- > 0) {
                        stacks.push(
                                BattleStack.createFromShipStack(f.ships[i], 
                                                                participant, 
                                                                f, 
                                                                position)
                        );
                    }
                        
                case StarbaseParticipant(s):
                    var design = switch (s.starbase) {
                        
                        case Design(d) : d;
                        
                        default:
                            throw "Error in battle engine, stars without "+
                                  "starbases shouldn't be included as " + 
                                  "participants.";
                    };
                    
                    var position = startingPositions.get(s.owner.id);
                    stacks.push(
                            BattleStack.createFromStarbase(design, 
                                                           participant, 
                                                           s, 
                                                           position)); 
            }
        }
        
        return stacks;
    }
    
    //returns a list of possible battles as groups of co-located fleets and 
    //starbases
    private static function determineBattles() : BattlesList {
        var allFleets = Global.fleets.getAllFleets();
        var battles = new BattlesList();
        
        for (fleet in allFleets) {
            if (fleet == null) continue; //allFleets includes nulls from empty
										 //id slots
            
            var sameLocation = new List<BattleParticipant>();
            
            //use a while loop here so that the elements can be modified while
            //looping
            var i = allFleets.length;
            while (i-- > 0) {
                if (allFleets[i] == null) continue;
                if (allFleets[i] == fleet) continue;
                
                if (fleet.location.distanceSquaredTo(allFleets[i].location) < 
                        DISTANCE_FOR_BATTLE) {
                            
                    sameLocation.add(FleetParticipant(allFleets[i]));
                    //null this as it can't be used in two separate battles
                    allFleets[i] = null;
                    
                }
            }
            
            //check starbases
            for (star in Global.stars) {
                if (star == null || star.starbase == null || star.owner == null 
                    || star.owner.id == Global.players.nullPlayer.id) continue;
                
                switch (star.starbase) {
                    
                    case None: continue;
                    
                    case Design(d): 
                        if (fleet.location.distanceSquaredTo(star.location) <
                                DISTANCE_FOR_BATTLE) {
                            sameLocation.add(StarbaseParticipant(star));
                        }
                }
            }
            
            //if there are fleets/starbases at this location, add the fleet to 
            //the list and add the list to the overall battles list
            if (sameLocation.length > 0) {
                sameLocation.add(FleetParticipant(fleet));
                battles.add(sameLocation);
            }
        }
        
        return battles;
    }
    
    //used with [List.filter], it removes battles that have no combatting 
    //fleets and also removes fleets that aren't involved in the fighting
    //from battle sites that do have conflicts.
    private static function filterPossibleBattles(
            possible : List<BattleParticipant>) : Bool {
        
        //filled initially with the players in the possible list as keys, and
        //a value of false. (Will be set to true if they're actually 
        //participating in the battle).
        var playersInvolved = new IntHash<Bool>();
        
        for (participant in possible) {
            switch (participant) {
                case FleetParticipant(f):
                    playersInvolved.set(f.owner.id, false);
                 case StarbaseParticipant(s):
                    playersInvolved.set(s.owner.id, false);
            }
        }
        
        var length = 0;
        for (player in playersInvolved) length++;
        if (length <= 1) return false;
        
        //try to find an enemy for each player. if one is found, then the 
        //playersInvolved is set to true
        //NOTE: Starbases don't have any battle plans and will only be included
        //if a player targets the starbase's owner.
        for (participant in possible) {
            switch (participant) {
                
                case StarbaseParticipant(s): //skip for now
                
                case FleetParticipant(f):
                    //try to find an enemy in each stack's battle plan
                    for (stack in f.ships) {
                        for (playerId in playersInvolved.keys()) {
                            if (playersInvolved.get(playerId)) continue;
                            
                            var relation = f.owner.relations.getRelationTo(
                                    Global.players.getPlayerById(playerId));
                            
                            if (stack.battlePlan.isBattleOpponent(relation)) {
                                playersInvolved.set(f.owner.id, true);
                                playersInvolved.set(playerId, true);    
                            }
                        }
                    }
            }
        }
        
        playersInBattle = new List();
        
        //if no one is targeted, then there is no battle. 
        //Also, remove the players who won't be involved in the battle.
        for (playerId in playersInvolved.keys()) {
            
            if (!playersInvolved.get(playerId)) {
                
                //remove participants that aren't targetted (or attacking)
                var possibleLeft = possible.filter( function(bp) {
                    return switch (bp) {
                        case FleetParticipant(f): f.owner.id != playerId;
                        case StarbaseParticipant(s) : s.owner.id != playerId;
                    }
                });
                
                //Need to do this manually as list.filter doesn't modify the 
                //list, only returns a new one
                possible.clear();
                for (participant in possibleLeft) possible.add(participant);
            } else {
                playersInBattle.add(Global.players.getPlayerById(playerId));
            }
        }
        
        //return false if no one's left for the battle
        return possible.length != 0;
    }
}
